<?php
class MyCodeCoverage {
	public $DSN = "mysql:dbname=coverage;host=localhost";
	public $USER = "coverage";
	public $PASSWORD = "pass";
	public $db;
	public $have_recorded = false;
	
	public function __construct() {
		try {
			$this->db = new PDO ( $this->DSN, $this->USER, $this->PASSWORD );
		} catch ( PDOException $e ) {
			echo 'Database connect error... ' . $e->getMessage ();
		}
	}
	
	// データベース初期化
	public function initialize($base_dir, $only_php = true) {
		$create_sql = "
      CREATE TABLE file_info (
        id int(10) unsigned primary key AUTO_INCREMENT,
        file_path varchar(1000),
        is_target integer default 0,
        ok_num integer default 0,
        ng_num integer default 0,
        nouse_num integer default 0,
        ok_rows text,
        ng_rows text,
        nouse_rows text,
        coverage_percents float,
        execute_count integer default 0,
        last_executed datetime
      )";
		
		$this->db->exec ( "DROP TABLE IF EXISTS file_info" );
		$this->db->exec ( $create_sql );
		
		if (! is_dir ( $base_dir )) {
			echo 'please specify correct base directory.';
			exit ();
		}
		
		$cmd = "find {$base_dir} -type f";
		if ($only_php) {
			$cmd .= " | grep 'php$'";
		}
		exec ( $cmd, $result, $ret );
		
		$sql = "INSERT INTO file_info(file_path, is_target) VALUES(?, 1)";
		$st = $this->db->prepare ( $sql );
		
		foreach ( $result as $file_path ) {
			$st->execute ( array (
					$file_path 
			) );
		}
	}
	
	// カバレッジ計測開始
	public function start() {
		xdebug_start_code_coverage ( XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE );
	}
	
	// カバレッジ計測終了
	public function stop() {
		$result = xdebug_get_code_coverage ();
		return $result;
	}
	
	// カバレッジ計測終了+DB登録
	public function stopAndRecord() {
		$result = $this->stop ();
		$this->recordResults ( $result );
	}
	
	// カバレッジ計測結果のDB登録
	public function recordResults($result) {
		if ($this->have_recorded) {
			return;
		}
		
		$select_sql = "SELECT * FROM file_info WHERE file_path = ?";
		$sst = $this->db->prepare ( $select_sql );
		
		foreach ( $result as $file_path => $cov_data ) {
			$sst->execute ( array (
					$file_path 
			) );
			$ex_data = $sst->fetch ( PDO::FETCH_ASSOC );
			
			if ($ex_data) {
				$new_data = $this->mergeData ( $cov_data, $ex_data );
				$this->updateById ( $new_data, $ex_data ['id'] );
			} else {
				$cov_data ['file_path'] = $file_path;
				$this->insert ( $cov_data );
			}
		}
		$this->have_recorded = true;
	}
	
	// ファイル単位で、既存の計測データと新たに計測したデータのマージ
	public function mergeData($coverage_data, $existing_data) {
		$cols = array (
				1 => 'ok_rows',
				- 1 => 'ng_rows',
				- 2 => 'nouse_rows' 
		);
		$row_arr = array (
				1 => array (),
				- 1 => array (),
				- 2 => array () 
		);
		
		foreach ( $cols as $key => $col ) {
			$row_arr [$key] = array ();
			
			if ($existing_data [$col]) {
				$tmp = explode ( ',', $existing_data [$col] );
				foreach ( $tmp as $row ) {
					$row_arr [$key] [$row] = $row;
				}
			}
		}
		
		// 0行目と(最終行+1)行目がカウントされるので、除外する。
		array_pop ( $coverage_data );
		unset ( $coverage_data [0] );
		
		// $resは、1 => 実行した, -1 => 実行しなかった, -2 => 使われない
		foreach ( $coverage_data as $row => $res ) {
			// 実行した行の時
			if ($res == 1) {
				$row_arr [$res] [$row] = $row;
				unset ( $row_arr [- 1] [$row] );
				unset ( $row_arr [- 2] [$row] );
				
				// 実行しなかった(されない)行の時
			} else {
				if ($existing_data ['execute_count'] >= 1 && isset ( $row_arr [1] [$row] )) {
					continue;
				}
				$row_arr [$res] [$row] = $row;
			}
		}
		foreach ( $row_arr as $key => $arr ) {
			ksort ( $row_arr [$key] );
		}
		
		$ret ['ok_num'] = count ( $row_arr [1] );
		$ret ['ng_num'] = count ( $row_arr [- 1] );
		$ret ['nouse_num'] = count ( $row_arr [- 2] );
		$ret ['ok_rows'] = implode ( ',', $row_arr [1] );
		$ret ['ng_rows'] = implode ( ',', $row_arr [- 1] );
		$ret ['nouse_rows'] = implode ( ',', $row_arr [- 2] );
		
		if ($ret ['ok_num'] + $ret ['ng_num']) {
			$ret ['coverage_percents'] = ( float ) $ret ['ok_num'] * 100 / ($ret ['ok_num'] + $ret ['ng_num']);
		} else {
			$ret ['coverage_percents'] = - 1;
		}
		$ret ['execute_count'] = $existing_data ['execute_count'] + 1;
		
		return $ret;
	}
	
	// ファイル単位の計測データ更新
	public function updateById($data, $id) {
		$columns = array (
				'ok_num',
				'ng_num',
				'nouse_num',
				'ok_rows',
				'ng_rows',
				'coverage_percents',
				'execute_count' 
		);
		$last_executed = date ( 'Y-m-d H:i:s' );
		$update_sql = "UPDATE file_info SET last_executed = '{$last_executed}', " . implode ( ' = ?, ', $columns ) . " = ? WHERE id = ?";
		
		foreach ( $columns as $col ) {
			$new_data [] = $data [$col];
		}
		$new_data [] = $id;
		return $this->db->prepare ( $update_sql )->execute ( $new_data );
	}
	
	// ファイル単位の計測データ新規作成
	public function insert($data) {
		$columns = array (
				'file_path',
				'ok_num',
				'ng_num',
				'nouse_num',
				'ok_rows',
				'ng_rows',
				'coverage_percents',
				'execute_count' 
		);
		$last_executed = date ( 'Y-m-d H:i:s' );
		$insert_sql = "INSERT INTO file_info (last_executed, " . implode ( ', ', $columns ) . ") VALUES ('{$last_executed}', " . str_repeat ( "?, ", count ( $columns ) - 1 ) . "?)";
		foreach ( $columns as $col ) {
			$new_data [] = $data [$col];
		}
		return $this->db->prepare ( $insert_sql )->execute ( $new_data );
	}
	
	// 計測データの集計対象フラグ更新
	public function changeTargets($ids, $is_target = 1) {
		if (! count ( $ids )) {
			return false;
		}
		if ($is_target) {
			$value = 1;
		} else {
			$value = 0;
		}
		$update_sql = "UPDATE file_info SET is_target = {$value} WHERE id IN (" . str_repeat ( '?, ', count ( $ids ) - 1 ) . "?)";
		return $this->db->prepare ( $update_sql )->execute ( $ids );
	}
	
	// 計測データリセット
	public function resetCoverage($ids) {
		if (! count ( $ids )) {
			return false;
		}
		$update_sql = "
      UPDATE file_info
      SET ok_num = 0, ng_num = 0, nouse_num = 0, ok_rows = null, ng_rows = null, nouse_rows = null, coverage_percents = 0, execute_count = 0
      WHERE id IN (" . str_repeat ( '?, ', count ( $ids ) - 1 ) . "?)";
		return $this->db->prepare ( $update_sql )->execute ( $ids );
	}
	
	// ファイル毎のカバレッジ結果一覧取得
	public function getDataList($order = null, $keyword = null, $cov = null, $is_target = 1) {
		$wheres = array ();
		$values = array ();
		$cov_where_list = array (
				0 => 'coverage_percents <= 0 OR coverage_percents IS NULL',
				1 => 'coverage_percents > 0 AND coverage_percents <= 30',
				2 => 'coverage_percents > 30 AND coverage_percents <= 70',
				3 => 'coverage_percents > 70 AND coverage_percents < 100',
				100 => 'coverage_percents = 100' 
		);
		
		switch ($order) {
			case "ok_num" :
				$order = "ok_num DESC";
				break;
			case "ng_num" :
				$order = "ng_num DESC";
				break;
			case "coverage_percents" :
				$order = "coverage_percents DESC";
				break;
			case "file_path" :
			default :
				$order = "file_path";
				break;
		}
		if ($is_target > 0) {
			$wheres [] = "is_target = 1";
		} else {
			$wheres [] = "is_target = 0";
		}
		if (strlen ( $keyword )) {
			$wheres [] = "file_path LIKE ?";
			$values [] = "%{$keyword}%";
		}
		if ($cov) {
			$or_wheres = array ();
			
			foreach ( $cov as $val ) {
				$or_wheres [] = '(' . $cov_where_list [$val] . ')';
			}
			if (count ( $or_wheres ) < count ( $cov_where_list )) {
				$wheres [] = '(' . implode ( ' OR ', $or_wheres ) . ')';
			}
		}
		
		$select_sql = "SELECT * FROM file_info";
		if (count ( $wheres )) {
			$select_sql .= " WHERE " . implode ( ' AND ', $wheres );
		}
		$select_sql .= " ORDER BY {$order}";
		
		$st = $this->db->prepare ( $select_sql );
		$st->execute ( $values );
		
		return $st->fetchAll ( PDO::FETCH_ASSOC );
	}
	
	// カバレッジ集計結果取得
	public function getStat() {
		$sql1 = "
      SELECT
        COUNT(*) AS total_files,
        SUM(ok_num) AS ok_sum,
        SUM(ng_num) AS ng_sum,
        SUM(nouse_num) AS nouse_sum
      FROM file_info
      WHERE is_target = 1
    ";
		$sql2 = "
      SELECT
        COUNT(*) AS not_executed_files
      FROM file_info
      WHERE is_target = 1 AND execute_count = 0
    ";
		$st1 = $this->db->prepare ( $sql1 );
		$st1->execute ();
		$ret1 = $st1->fetch ( PDO::FETCH_ASSOC );
		$st2 = $this->db->prepare ( $sql2 );
		$st2->execute ();
		$ret2 = $st2->fetch ( PDO::FETCH_ASSOC );
		
		$ret = array_merge ( $ret1, $ret2 );
		$ret ['executed_files'] = $ret ['total_files'] - $ret ['not_executed_files'];
		$ret ['total_sum'] = $ret ['ok_sum'] + $ret ['ng_sum'] + $ret ['nouse_sum'];
		return $ret;
	}
	
	/**
	 * ファイル詳細表示用のデータ取得
	 * 
	 * @return array( 'db_data' => ...,
	 *         1 => array('content' => ..., [ok => 1, ng => 1]),
	 *         [2 => array(...)],
	 *         [3 => ...],
	 *         ...
	 *         )
	 */
	public function getDetailById($id) {
		$st = $this->db->prepare ( "SELECT * FROM file_info WHERE id = ?" );
		$st->execute ( array (
				$id 
		) );
		$data = $st->fetch ( PDO::FETCH_ASSOC );
		
		if (! $data)
			return false;
		
		$ret ['db_data'] = $data;
		$ok_rows = array ();
		$ng_rows = array ();
		
		if (strlen ( $data ['ok_rows'] )) {
			$tmp = explode ( ',', $data ['ok_rows'] );
			foreach ( $tmp as $row ) {
				$ok_rows [$row] = $row;
			}
		}
		if (strlen ( $data ['ng_rows'] )) {
			$tmp = explode ( ',', $data ['ng_rows'] );
			foreach ( $tmp as $row ) {
				$ng_rows [$row] = $row;
			}
		}
		
		$fp = fopen ( $data ['file_path'], 'r' );
		$num = 1;
		
		while ( ($row = fgets ( $fp )) ) {
			$ret [$num] ['content'] = $row;
			
			if (isset ( $ok_rows [$num] )) {
				$ret [$num] ['ok'] = 1;
			} else if (isset ( $ng_rows [$num] ) || ! $data ['execute_count']) {
				$ret [$num] ['ng'] = 1;
			}
			
			$num ++;
		}
		return $ret;
	}
}


